/*****************************************************************************
 * Free42 -- an HP-42S calculator simulator
 * Copyright (C) 2004-2010  Thomas Okken
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see http://www.gnu.org/licenses/.
 *****************************************************************************/

// shell.cpp : Defines the entry point for the application.
//

#include <sys/types.h>
#include <sys/stat.h>
#include <direct.h>
#include <stdio.h>
#include <shlobj.h>

#include "stdafx.h"
#include "resource.h"

#include "free42.h"
#include "shell.h"
#include "shell_skin.h"
#include "shell_spool.h"
#include "core_main.h"
#include "core_display.h"
#include "msg2string.h"


#define MAX_LOADSTRING 100
#define FILENAMELEN 256

// The maximum height of a Bitmap is 32767 pixels;
// so, if I want to use a larger buffer, I'll have to
// change repaint_printout() so that it creates
// its Bitmap objects a bit more cleverly. TODO.

#define PRINT_LINES 16384
#define PRINT_BYTESPERLINE 36
#define PRINT_SIZE 589824
/*
#define PRINT_LINES 32767
#define PRINT_BYTESPERLINE 36
#define PRINT_SIZE 1179612
*/

/**********************************************************/
/* Linked-in skins; defined in the skins.c, which in turn */
/* is generated by skin2c.c under control of skin2c.conf  */
/**********************************************************/

extern int skin_count;
extern const TCHAR *skin_name[];
extern const long skin_layout_size[];
extern const char *skin_layout_data[];
extern const long skin_bitmap_size[];
extern const char *skin_bitmap_data[];


// Global Variables:
static HINSTANCE hInst;                             // current instance
static HWND hMainWnd;                               // our main window
static HWND hPrintOutWnd;                           // our print-out window
static TCHAR szMainTitle[MAX_LOADSTRING];           // The main title bar text
static TCHAR szPrintOutTitle[MAX_LOADSTRING];       // The print-out title bar text
static TCHAR szMainWindowClass[MAX_LOADSTRING];     // The main window class
static TCHAR szPrintOutWindowClass[MAX_LOADSTRING]; // The print-out window class

static UINT timer = 0;
static UINT timer3 = 0;
static int running = 0;
static int enqueued = 0;

static int printout_top;
static int printout_bottom;
static int printout_pos;
static char *printout;

static int ckey = 0;
static int skey;
static unsigned char *macro;
static int active_keycode = 0;
static bool ctrl_down = false;
static bool alt_down = false;
static bool shift_down = false;
static bool just_pressed_shift = false;
static bool mouse_key;

static int keymap_length = 0;
static keymap_entry *keymap = NULL;


#define SHELL_VERSION 7

typedef struct state {
	BOOL extras;
	WINDOWPLACEMENT mainPlacement;
	int mainPlacementValid;
	WINDOWPLACEMENT printOutPlacement;
	int printOutPlacementValid;
	int printOutOpen;
	int printerToTxtFile;
	int printerToGifFile;
	char printerTxtFileName[FILENAMELEN];
	char printerGifFileName[FILENAMELEN];
	int printerGifMaxLength;
	char skinName[FILENAMELEN];
	BOOL alwaysOnTop;
	BOOL singleInstance;
	BOOL calculatorKey;
} state_type;

static state_type state;
static int placement_saved = 0;
static int printOutWidth;
static int printOutHeight;

static char free42dirname[FILENAMELEN];
static char statefilename[FILENAMELEN];
static FILE *statefile = NULL;
static char printfilename[FILENAMELEN];

static FILE *print_txt = NULL;
static FILE *print_gif = NULL;
static char print_gif_name[FILENAMELEN];
static int gif_seq = -1;
static int gif_lines;

static int sel_prog_count;
static int *sel_prog_list;
static char export_file_name[FILENAMELEN];
static FILE *export_file = NULL;
static FILE *import_file = NULL;

static int ann_updown = 0;
static int ann_shift = 0;
static int ann_print = 0;
static int ann_run = 0;
static int ann_battery = 0;
static int ann_g = 0;
static int ann_rad = 0;


// Foward declarations of functions included in this code module:
static void MyRegisterClass(HINSTANCE hInstance);
static BOOL InitInstance(HINSTANCE, int);
static LRESULT CALLBACK	MainWndProc(HWND, UINT, WPARAM, LPARAM);
static LRESULT CALLBACK PrintOutWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK	About(HWND, UINT, WPARAM, LPARAM);
static LRESULT CALLBACK	ExportProgram(HWND, UINT, WPARAM, LPARAM);
static int browse_file(HWND owner, char *title, int save, char *filter, char *defExt, char *buf, int buflen);
static LRESULT CALLBACK	Preferences(HWND, UINT, WPARAM, LPARAM);
static void set_home_dir(const char *path);
static void get_home_dir(char *path, int pathlen, BOOL exedir_ok);
static void config_home_dir(HWND owner, char *buf, int bufsize);
static void mapCalculatorKey();
static void copy();
static void paste();
static void Quit();

static VOID CALLBACK repeater(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime);
static VOID CALLBACK timeout1(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime);
static VOID CALLBACK timeout2(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime);
static VOID CALLBACK timeout3(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime);
static VOID CALLBACK battery_checker(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime);

static void show_printout();
static void export_program();
static void import_program();
static void clear_printout();
static void repaint_printout(int x, int y, int width, int height, int validate);
static void repaint_printout(HDC hdc, int destpos, int x, int y, int width, int height, int validate);
static void printout_scrolled(int offset);
static void printout_scroll_to_bottom(int offset);
static void printout_length_changed();

static void read_key_map(const char *keymapfilename);
static void init_shell_state(int4 version);
static int read_shell_state(int4 *version);
static int write_shell_state();
static void txt_writer(const char *text, int length);
static void txt_newliner();
static void gif_seeker(int4 pos);
static void gif_writer(const char *text, int length);


int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
	MSG msg;
	HACCEL hAccelTable;

	// Initialize global strings
#ifdef BCD_MATH
	LoadString(hInstance, IDS_APP_TITLE_DEC, szMainTitle, MAX_LOADSTRING);
#else
	LoadString(hInstance, IDS_APP_TITLE_BIN, szMainTitle, MAX_LOADSTRING);
#endif
	LoadString(hInstance, IDS_PRINTOUT_TITLE, szPrintOutTitle, MAX_LOADSTRING);
	LoadString(hInstance, IDC_FREE42, szMainWindowClass, MAX_LOADSTRING);
	LoadString(hInstance, IDC_FREE42_PRINTOUT, szPrintOutWindowClass, MAX_LOADSTRING);

	MyRegisterClass(hInstance);

	// Perform application initialization:
	if (!InitInstance (hInstance, nCmdShow)) 
	{
		return FALSE;
	}

	hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_FREE42);

	// Main message loop:
	while (1) {
		while (running && !PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
			int dummy1, dummy2;
			running = core_keydown(0, &dummy1, &dummy2);
		}
		if (!GetMessage(&msg, NULL, 0, 0)) 
			break;
		if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	Quit();
	return msg.wParam;
}



//
//  FUNCTION: MyRegisterClass()
//
//  PURPOSE: Registers the window class.
//
//  COMMENTS:
//
//    This function and its usage is only necessary if you want this code
//    to be compatible with Win32 systems prior to the 'RegisterClassEx'
//    function that was added to Windows 95. It is important to call this function
//    so that the application will get 'well formed' small icons associated
//    with it.
//
static void MyRegisterClass(HINSTANCE hInstance)
{
	WNDCLASSEX wcex1, wcex2;

	wcex1.cbSize = sizeof(WNDCLASSEX); 

	wcex1.style			= CS_HREDRAW | CS_VREDRAW;
	wcex1.lpfnWndProc	= (WNDPROC) MainWndProc;
	wcex1.cbClsExtra	= 0;
	wcex1.cbWndExtra	= 0;
	wcex1.hInstance		= hInstance;
	wcex1.hIcon			= LoadIcon(hInstance, (LPCTSTR) IDI_FREE42);
	wcex1.hCursor		= LoadCursor(NULL, IDC_ARROW);
	wcex1.hbrBackground	= (HBRUSH) (COLOR_WINDOW+1);
	wcex1.lpszMenuName	= (LPCSTR) IDC_FREE42;
	wcex1.lpszClassName	= szMainWindowClass;
	wcex1.hIconSm		= LoadIcon(wcex1.hInstance, (LPCTSTR) IDI_SMALL);

	RegisterClassEx(&wcex1);

	wcex2.cbSize = sizeof(WNDCLASSEX); 

	wcex2.style			= CS_HREDRAW | CS_VREDRAW;
	wcex2.lpfnWndProc	= (WNDPROC) PrintOutWndProc;
	wcex2.cbClsExtra	= 0;
	wcex2.cbWndExtra	= 0;
	wcex2.hInstance		= hInstance;
	wcex2.hIcon			= LoadIcon(hInstance, (LPCTSTR) IDI_FREE42);
	wcex2.hCursor		= LoadCursor(NULL, IDC_ARROW);
	wcex2.hbrBackground	= (HBRUSH) (COLOR_WINDOW+1);
	wcex2.lpszMenuName	= NULL;
	wcex2.lpszClassName	= szPrintOutWindowClass;
	wcex2.hIconSm		= LoadIcon(wcex2.hInstance, (LPCTSTR) IDI_SMALL);

	RegisterClassEx(&wcex2);
}

//
//   FUNCTION: InitInstance(HANDLE, int)
//
//   PURPOSE: Saves instance handle and creates main window
//
//   COMMENTS:
//
//        In this function, we save the instance handle in a global variable and
//        create and display the main program window.
//
static BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	RECT r;

	hInst = hInstance; // Store instance handle in our global variable

	srand(GetTickCount());

    /**********************************************/
    /***** Try to create the Free42 directory *****/
    /**********************************************/

	get_home_dir(free42dirname, FILENAMELEN, TRUE);
    _mkdir(free42dirname);

	char keymapfilename[FILENAMELEN];
	sprintf(statefilename, "%s\\state.bin", free42dirname);
	sprintf(printfilename, "%s\\print.bin", free42dirname);
	sprintf(keymapfilename, "%s\\keymap.txt", free42dirname);

	read_key_map(keymapfilename);

	printout = (char *) malloc(PRINT_SIZE);
	// TODO - handle memory allocation failure
	FILE *printfile = fopen(printfilename, "rb");
    if (printfile != NULL) {
		int n = fread(&printout_bottom, 1, sizeof(int), printfile);
		if (n == sizeof(int)) {
			int bytes = printout_bottom * PRINT_BYTESPERLINE;
			n = fread(printout, 1, bytes, printfile);
			if (n != bytes)
				printout_bottom = 0;
		} else
			printout_bottom = 0;
		fclose(printfile);
    } else
		printout_bottom = 0;
    printout_top = 0;
    for (int n = printout_bottom * PRINT_BYTESPERLINE; n < PRINT_SIZE; n++)
		printout[n] = 0;
	printout_pos = printout_bottom;

	int init_mode;
	int4 version;

    statefile = fopen(statefilename, "rb");
    if (statefile != NULL) {
		if (read_shell_state(&version))
			init_mode = 1;
		else {
			init_shell_state(-1);
			init_mode = 2;
		}
    } else {
		init_shell_state(-1);
		init_mode = 0;
    }

	if (state.singleInstance) {
		HWND hPrevWnd = FindWindow(szMainWindowClass, szMainTitle);
		if (hPrevWnd != NULL) {
			if (IsIconic(hPrevWnd))
				OpenIcon(hPrevWnd);
			SetForegroundWindow(hPrevWnd);
			return FALSE;
		}
	}

	skin_load(state.skinName, free42dirname, &r.right, &r.bottom);
	r.top = 0;
	r.left = 0;
	AdjustWindowRect(&r, WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX|WS_OVERLAPPED, 1);

	hMainWnd = CreateWindow(szMainWindowClass, szMainTitle,
							WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX|WS_OVERLAPPED,
							CW_USEDEFAULT, 0, r.right - r.left, r.bottom - r.top,
							NULL, NULL, hInstance, NULL);

	if (hMainWnd == NULL)
		return FALSE;

	core_init(init_mode, version);
	if (statefile != NULL) {
		fclose(statefile);
		statefile = NULL;
	}

	if (state.mainPlacementValid) {
		// Fix the size, in case the saved settings are not appropriate
		// for the current desktop properties (the decor dimensions may
		// be different).
		RECT *r2 = &state.mainPlacement.rcNormalPosition;
		r2->right = r2->left + r.right - r.left;
		r2->bottom = r2->top + r.bottom - r.top;
		SetWindowPlacement(hMainWnd, &state.mainPlacement);
	}
	if (state.alwaysOnTop)
		SetWindowPos(hMainWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
	ShowWindow(hMainWnd, nCmdShow);
	UpdateWindow(hMainWnd);

	if (state.printOutOpen) {
		show_printout();
		BringWindowToTop(hMainWnd);
	}

	if (core_powercycle())
		running = 1;
	SetTimer(NULL, 0, 60000, battery_checker);

	return TRUE;
}

static void shell_keydown() {
	if (ckey != 0) {
		HDC hdc = GetDC(hMainWnd);
		HDC memdc = CreateCompatibleDC(hdc);
		if (skey == -1)
			skey = skin_find_skey(ckey);
		skin_repaint_key(hdc, memdc, skey, 1);
		DeleteObject(memdc);
		ReleaseDC(hMainWnd, hdc);
	}
	if (timer != 0) {
		KillTimer(NULL, timer);
		timer = 0;
	}
	if (timer3 != 0 && (macro != NULL || ckey != 28 /* SHIFT */)) {
		KillTimer(NULL, timer3);
		timer3 = 0; 
		core_timeout3(0);
	}
	int repeat;
	if (macro != NULL) {
		if (*macro == 0) {
			squeak();
			return;
		}
		bool one_key_macro = macro[1] == 0 || (macro[2] == 0 && macro[0] == 28);
		if (!one_key_macro)
			skin_display_set_enabled(false);
		while (*macro != 0) {
			running = core_keydown(*macro++, &enqueued, &repeat);
			if (*macro != 0 && !enqueued)
				core_keyup();
		}
		if (!one_key_macro) {
			skin_display_set_enabled(true);
			HDC hdc = GetDC(hMainWnd);
			HDC memdc = CreateCompatibleDC(hdc);
			skin_repaint_display(hdc, memdc);
			skin_repaint_annunciator(hdc, memdc, 1, ann_updown);
			skin_repaint_annunciator(hdc, memdc, 2, ann_shift);
			skin_repaint_annunciator(hdc, memdc, 3, ann_print);
			skin_repaint_annunciator(hdc, memdc, 4, ann_run);
			skin_repaint_annunciator(hdc, memdc, 5, ann_battery);
			skin_repaint_annunciator(hdc, memdc, 6, ann_g);
			skin_repaint_annunciator(hdc, memdc, 7, ann_rad);
			DeleteDC(memdc);
			ReleaseDC(hMainWnd, hdc);
			repeat = 0;
		}
	} else
		running = core_keydown(ckey, &enqueued, &repeat);
	if (!running) {
		if (repeat != 0)
			timer = SetTimer(NULL, 0, repeat == 1 ? 1000 : 500, repeater);
		else if (!enqueued)
			timer = SetTimer(NULL, 0, 250, timeout1);
	}
}

static void shell_keyup() {
	HDC hdc = GetDC(hMainWnd);
	HDC memdc = CreateCompatibleDC(hdc);
	skin_repaint_key(hdc, memdc, skey, 0);
	DeleteObject(memdc);
	ReleaseDC(hMainWnd, hdc);
	ckey = 0;
	skey = -1;
	if (timer != 0) {
		KillTimer(NULL, timer);
		timer = 0;
	}
	if (!enqueued)
		running = core_keyup();
}

//
//  FUNCTION: WndProc(HWND, unsigned, WORD, LONG)
//
//  PURPOSE:  Processes messages for the main window.
//
//  WM_COMMAND	- process the application menu
//  WM_PAINT	- Paint the main window
//  WM_DESTROY	- post a quit message and return
//
//
static LRESULT CALLBACK MainWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
	//static FILE *logfile = fopen("C:\\Windows\\Desktop\\log.txt", "w");
	//fprintf(logfile, "message=%s wParam=0x%x lParam=0x%lx\n", msg2string(message), wParam, lParam);
	//fflush(logfile);

	switch (message) {
		case WM_COMMAND: {
			int wmId    = LOWORD(wParam); 
			int wmEvent = HIWORD(wParam); 
			// Parse the menu selections:
			switch (wmId) {
				case IDM_SHOWPRINTOUT:
					show_printout();
					break;
				case IDM_EXPORTPROGRAM:
					export_program();
					break;
				case IDM_IMPORTPROGRAM:
					import_program();
					break;
				case IDM_CLEARPRINTOUT:
					clear_printout();
					break;
				case IDM_PREFERENCES:
					DialogBox(hInst, (LPCTSTR)IDD_PREFERENCES, hWnd, (DLGPROC)Preferences);
					break;
				case IDM_EXIT:
					DestroyWindow(hWnd);
					break;
				case ID_EDIT_COPY:
					copy();
					break;
				case ID_EDIT_PASTE:
					paste();
					break;
				case IDM_ABOUT:
					DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
					break;
				default:
					if (wmId >= 40000) {
						// 'Skin' menu
						HMENU mainmenu = GetMenu(hWnd);
						HMENU skinmenu = GetSubMenu(mainmenu, 2);
						MENUITEMINFO mii;
						mii.cbSize = sizeof(MENUITEMINFO);
						mii.fMask = MIIM_TYPE;
						mii.cch = FILENAMELEN;
						mii.dwTypeData = state.skinName;
						GetMenuItemInfo(skinmenu, wmId, FALSE, &mii);

						// The following is really just skin_load(), followed by
						// resizing the window. Unfortunately, I couldn't find a
						// function that resizes a window without setting its position
						// at the same time, hence the contortions.
						RECT r;
						GetWindowRect(hWnd, &r);
						long width, height;
						skin_load(state.skinName, free42dirname, &width, &height);
						core_repaint_display();
						r.right = r.left + width;
						r.bottom = r.top + height;
						LONG dx = r.left;
						LONG dy = r.top;
						AdjustWindowRect(&r, WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX|WS_OVERLAPPED, 1);
						dx -= r.left;
						dy -= r.top;
						OffsetRect(&r, dx, dy);
						MoveWindow(hWnd, r.left, r.top, r.right - r.left, r.bottom - r.top, TRUE);
						SetRect(&r, 0, 0, width, height);
						InvalidateRect(hWnd, &r, FALSE);
						break;
					}
					return DefWindowProc(hWnd, message, wParam, lParam);
			}
			break;
		}
		case WM_PAINT: {
			PAINTSTRUCT ps;
			HDC hdc = BeginPaint(hWnd, &ps);
			HDC memdc = CreateCompatibleDC(hdc);
			skin_repaint(hdc, memdc);
			skin_repaint_display(hdc, memdc);
			skin_repaint_annunciator(hdc, memdc, 1, ann_updown);
			skin_repaint_annunciator(hdc, memdc, 2, ann_shift);
			skin_repaint_annunciator(hdc, memdc, 3, ann_print);
			skin_repaint_annunciator(hdc, memdc, 4, ann_run);
			skin_repaint_annunciator(hdc, memdc, 5, ann_battery);
			skin_repaint_annunciator(hdc, memdc, 6, ann_g);
			skin_repaint_annunciator(hdc, memdc, 7, ann_rad);
			if (ckey != 0)
				skin_repaint_key(hdc, memdc, skey, 1);
			DeleteDC(memdc);
			EndPaint(hWnd, &ps);
			break;
		}
		case WM_LBUTTONDOWN: {
			if (ckey == 0) {
				int x = LOWORD(lParam);  // horizontal position of cursor
				int y = HIWORD(lParam);  // vertical position of cursor
				skin_find_key(x, y, ann_shift != 0, &skey, &ckey);
				if (ckey != 0) {
					macro = skin_find_macro(ckey);
					shell_keydown();
					mouse_key = true;
				}
			}
			break;
		}
		case WM_LBUTTONUP:
			if (ckey != 0 && mouse_key)
				shell_keyup();
			break;
		case WM_KEYDOWN:
		case WM_CHAR:
		case WM_SYSKEYDOWN:
		case WM_SYSCHAR: {
			static int virtKey = 0;
			int keyChar;
			if ((lParam & (1 << 30)) != 0)
				// Auto-repeat event; ignore.
				break;
			if (message == WM_KEYDOWN || message == WM_SYSKEYDOWN) {
				keyChar = 0;
				virtKey = (int) wParam;
			} else
				keyChar = (int) wParam;
			just_pressed_shift = false;
			if (virtKey == 17) {
				ctrl_down = true;
				goto do_default;
			} else if (virtKey == 18) {
				alt_down = true;
				goto do_default;
			} else if (virtKey == 16) {
				shift_down = true;
				just_pressed_shift = true;
				goto do_default;
			}
			if ((message == WM_KEYDOWN || message == WM_SYSKEYDOWN)
					&& !ctrl_down
					&& (virtKey == 8 // Backspace
					|| virtKey == 9 // Tab
					|| virtKey == 13 // Enter
					|| (virtKey == 27 && !shift_down) // Escape
					|| virtKey == 32 // Space
					|| (virtKey >= 48 && virtKey < 112)
					|| (virtKey >= 0xB0 && virtKey < 0xF0)))
				// Keystrokes that will be followed by a WM_CHAR
				// message; we defer handling them until then.
				break;

			if (ckey == 0 || !mouse_key) {
				int i;
				bool printable = keyChar >= 32 && keyChar <= 126;
				if (ckey != 0) {
					shell_keyup();
					active_keycode = 0;
				}

				bool exact;
				bool cshift_down = ann_shift != 0;
				unsigned char *key_macro = skin_keymap_lookup(virtKey, ctrl_down, alt_down, shift_down, cshift_down, &exact);
				if (key_macro == NULL || !exact) {
					for (i = 0; i < keymap_length; i++) {
						keymap_entry *entry = keymap + i;
						if (ctrl_down == entry->ctrl
								&& alt_down == entry->alt
								&& shift_down == entry->shift
								&& virtKey == entry->keycode) {
							if (cshift_down == entry->cshift) {
								key_macro = entry->macro;
								break;
							} else {
								if (key_macro == NULL)
									key_macro = entry->macro;
							}
						}
					}
				}

				if (key_macro == NULL || (key_macro[0] != 36 || key_macro[1] != 0)
						&& (key_macro[0] != 28 || key_macro[1] != 36 || key_macro[2] != 0)) {
					// The test above is to make sure that whatever mapping is in
					// effect for R/S will never be overridden by the special cases
					// for the ALPHA and A..F menus.
					if (printable && core_alpha_menu()) {
						if (keyChar >= 'a' && keyChar <= 'z')
							keyChar = keyChar + 'A' - 'a';
						else if (keyChar >= 'A' && keyChar <= 'Z')
							keyChar = keyChar + 'a' - 'A';
						ckey = 1024 + keyChar;
						skey = -1;
						macro = NULL;
						shell_keydown();
						mouse_key = false;
						active_keycode = virtKey;
						break;
					} else if (core_hex_menu() && ((keyChar >= 'a' && keyChar <= 'f')
								|| (keyChar >= 'A' && keyChar <= 'F'))) {
						if (keyChar >= 'a' && keyChar <= 'f')
							ckey = keyChar - 'a' + 1;
						else
							ckey = keyChar - 'A' + 1;
						skey = -1;
						macro = NULL;
						shell_keydown();
						mouse_key = false;
						active_keycode = virtKey;
						break;
					}
				}

				if (key_macro != NULL) {
					// A keymap entry is a sequence of zero or more calculator
					// keystrokes (1..37) and/or macros (38..255). We expand
					// macros here before invoking shell_keydown().
					// If the keymap entry is one key, or two keys with the
					// first being 'shift', we highlight the key in question
					// by setting ckey; otherwise, we set ckey to -10, which
					// means no skin key will be highlighted.
					ckey = -10;
					skey = -1;
					if (key_macro[0] != 0)
						if (key_macro[1] == 0)
							ckey = key_macro[0];
						else if (key_macro[2] == 0 && key_macro[0] == 28)
							ckey = key_macro[1];
					bool needs_expansion = false;
					for (int j = 0; key_macro[j] != 0; j++)
						if (key_macro[j] > 37) {
							needs_expansion = true;
							break;
						}
					if (needs_expansion) {
						static unsigned char macrobuf[1024];
						int p = 0;
						for (int j = 0; key_macro[j] != 0 && p < 1023; j++) {
							int c = key_macro[j];
							if (c <= 37)
								macrobuf[p++] = c;
							else {
								unsigned char *m = skin_find_macro(c);
								if (m != NULL)
									while (*m != 0 && p < 1023)
										macrobuf[p++] = *m++;
							}
						}
						macrobuf[p] = 0;
						macro = macrobuf;
					} else
						macro = key_macro;
					shell_keydown();
					mouse_key = false;
					active_keycode = virtKey;
					break;
				}
			}
			goto do_default;
		}
		case WM_KEYUP:
		case WM_SYSKEYUP: {
			int virtKey = (int) wParam;
			if (virtKey == 17) {
				ctrl_down = false;
				goto do_default;
			} else if (virtKey == 18) {
				alt_down = false;
				goto do_default;
			} else if (virtKey == 16) {
				shift_down = false;
				if (ckey == 0 && just_pressed_shift) {
					ckey = 28;
					skey = 1;
					macro = NULL;
					shell_keydown();
					shell_keyup();
				}
				goto do_default;
			}
			if (ckey != 0 && !mouse_key && virtKey == active_keycode) {
				shell_keyup();
				active_keycode = 0;
			}
			goto do_default;
		}
		case WM_DESTROY:
			GetWindowPlacement(hMainWnd, &state.mainPlacement);
			state.mainPlacementValid = 1;
			if (state.printOutOpen) {
				GetWindowPlacement(hPrintOutWnd, &state.printOutPlacement);
				state.printOutPlacementValid = 1;
			}
			placement_saved = 1;
			PostQuitMessage(0);
			break;
		case WM_INITMENUPOPUP: {
			if (HIWORD(lParam) != 0 || LOWORD(lParam) != 2)
				// HIWORD(lParam) is nonzero if the system menu is selected;
				// LOWORD(lParam) is the index of the selected menu.
				// I want to update the 'Skin' menu, which is at index 2;
				// any other index I don't care about, since those other menus
				// are all static.
				break;
			HMENU menu = (HMENU) wParam;
			UINT id = 40000;
			if (state.skinName[0] == 0)
				strcpy(state.skinName, skin_name[0]);

			int i;
			for (i = GetMenuItemCount(menu) - 1; i >= 0; i--)
				RemoveMenu(menu, i, MF_BYPOSITION);

			for (i = 0; i < skin_count; i++) {
				UINT flags = 0;
				if (strcmp(state.skinName, skin_name[i]) == 0)
					flags = MF_CHECKED;
				AppendMenu(menu, flags, id++, skin_name[i]);
			}

			int have_separator = 0;
			char path[MAX_PATH];
			path[MAX_PATH - 1] = 0;
			WIN32_FIND_DATA wfd;
			int n = 0;
			char name[100][MAX_PATH];

			strncpy(path, free42dirname, MAX_PATH - 1);
			strncat(path, "\\*.layout", MAX_PATH - 1);
			path[MAX_PATH - 1] = 0;
			HANDLE search = FindFirstFile(path, &wfd);
			if (search != INVALID_HANDLE_VALUE) {
				do {
					if ((wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
						wfd.cFileName[strlen(wfd.cFileName) - 7] = 0;
						strcpy(name[n++], wfd.cFileName);
					}
				} while (FindNextFile(search, &wfd));
				FindClose(search);
			}

			// Search executable's directory
			char exedir[MAX_PATH];
			GetModuleFileName(0, exedir, MAX_PATH - 1);
			char *lastbackslash = strrchr(exedir, '\\');
			if (lastbackslash != 0)
				*lastbackslash = 0;
			else
				strcpy(exedir, "C:");
			if (_stricmp(exedir, free42dirname) != 0) {
				strncat(exedir, "\\*.layout", MAX_PATH - 1);
				exedir[MAX_PATH - 1] = 0;
				search = FindFirstFile(exedir, &wfd);
				if (search != INVALID_HANDLE_VALUE) {
					do {
						if ((wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
							wfd.cFileName[strlen(wfd.cFileName) - 7] = 0;
							strcpy(name[n++], wfd.cFileName);
						}
					} while (FindNextFile(search, &wfd));
					FindClose(search);
				}
			}

			qsort(name, n, MAX_PATH, (int (*)(const void *, const void *)) _stricmp);
			for (i = 0; i < n; i++) {
				UINT flags;
				int j;
				if (i > 0 && _stricmp(name[i], name[i - 1]) == 0)
					goto skip;
				for (j = 0; j < skin_count; j++)
					if (_stricmp(name[i], skin_name[j]) == 0)
						goto skip;
				if (!have_separator) {
					AppendMenu(menu, MF_SEPARATOR, 0, NULL);
					have_separator = 1;
				}
				flags = 0;
				if (_stricmp(state.skinName, name[i]) == 0)
					flags = MF_CHECKED;
				AppendMenu(menu, flags, id++, name[i]);
				skip:;
			}
			break;
		}
		default:
		do_default:
			return DefWindowProc(hWnd, message, wParam, lParam);
   }
   return 0;
}

//
//  FUNCTION: PrintOutWndProc(HWND, unsigned, WORD, LONG)
//
//  PURPOSE:  Processes messages for the print-out window.
//
//  WM_COMMAND	- process the application menu
//  WM_PAINT	- Paint the main window
//  WM_DESTROY	- post a quit message and return
//
//
static LRESULT CALLBACK PrintOutWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) 
	{
		case WM_PAINT: {
			RECT r;
			if (GetUpdateRect(hWnd, &r, FALSE)) {
				PAINTSTRUCT ps;
				HDC hdc = BeginPaint(hWnd, &ps);
				repaint_printout(hdc, 1, r.left, r.top, r.right - r.left, r.bottom - r.top, 0);
				EndPaint(hWnd, &ps);
				break;
			}
		}
		case WM_DESTROY:
			GetWindowPlacement(hPrintOutWnd, &state.printOutPlacement);
			state.printOutPlacementValid = 1;
			state.printOutOpen = 0;
			hPrintOutWnd = NULL;
			return 0;
		case WM_SIZING: {
			LPRECT r = (LPRECT) lParam;
			switch (wParam) {
				case WMSZ_BOTTOMLEFT:
				case WMSZ_TOPLEFT:
				case WMSZ_LEFT:
					r->left = r->right - printOutWidth;
					break;
				case WMSZ_BOTTOMRIGHT:
				case WMSZ_TOPRIGHT:
				case WMSZ_RIGHT:
					r->right = r->left + printOutWidth;
			}
			return 1;
		}
		case WM_SIZE:
			if (wParam == SIZE_MAXIMIZED || wParam == SIZE_RESTORED) {
				printOutHeight = HIWORD(lParam);
				printout_length_changed();
			}
			return 0;
		case WM_VSCROLL: {
			int scroll_code = LOWORD(wParam);
			SCROLLINFO si;
			si.cbSize = sizeof(SCROLLINFO);
			si.fMask = SIF_ALL;
			GetScrollInfo(hPrintOutWnd, SB_VERT, &si);
			int pos;
			int p = si.nPage;
			if (p > 0)
				p--;
			int maxpos = si.nMax - p;
			switch (scroll_code) {
				case SB_TOP: pos = si.nMin; break;
				case SB_BOTTOM: pos = si.nMax; break;
				case SB_LINEUP: pos = printout_pos - 18; break;
				case SB_LINEDOWN: pos = printout_pos + 18; break;
				case SB_PAGEUP: pos = printout_pos - printOutHeight + 18; break;
				case SB_PAGEDOWN: pos = printout_pos + printOutHeight - 18; break;
				case SB_THUMBPOSITION:
				case SB_THUMBTRACK: pos = HIWORD(wParam); break;
				default: pos = printout_pos;
			}
			if (pos < 0)
				pos = 0;
			else if (pos > maxpos)
				pos = maxpos;
			if (pos != printout_pos) {
				int oldpos = printout_pos;
				printout_pos = pos;
				SetScrollPos(hPrintOutWnd, SB_VERT, printout_pos, TRUE);
				printout_scrolled(oldpos - printout_pos);
			}
			return 0;
		}
		default:
			return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

// Mesage handler for about box.
static LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
		case WM_INITDIALOG:
			return TRUE;

		case WM_COMMAND:
			if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) 
			{
				EndDialog(hDlg, LOWORD(wParam));
				return TRUE;
			}
			break;
	}
    return FALSE;
}

// Mesage handler for Export Program dialog.
static LRESULT CALLBACK ExportProgram(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) {
		case WM_INITDIALOG: {
			HWND list = GetDlgItem(hDlg, IDC_LIST1);
			char buf[10000];
			int count = core_list_programs(buf, 10000);
			char *p = buf;
			for (int i = 0; i < count; i++) {
				SendMessage(list, LB_ADDSTRING, 0, (long) p);
				p += strlen(p) + 1;
			}
			return TRUE;
		}
		case WM_COMMAND: {
			int cmd = LOWORD(wParam);
			switch (cmd) {
				case IDOK: {
					HWND list = GetDlgItem(hDlg, IDC_LIST1);
					sel_prog_count = SendMessage(list, LB_GETSELCOUNT, 0, 0);
					if (sel_prog_count > 0) {
						sel_prog_list = (int *) malloc(sel_prog_count * sizeof(int));
						// TODO - handle memory allocation failure
						SendMessage(list, LB_GETSELITEMS, sel_prog_count, (long) sel_prog_list);
					}
					EndDialog(hDlg, 1);
					return TRUE;
				}
				case IDCANCEL: {
					EndDialog(hDlg, 0);
					return FALSE;
				}
			}
			return FALSE;
		}
	}
	return FALSE;
}

// Mesage handler for preferences dialog.
static LRESULT CALLBACK Preferences(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	// TODO: track focus changes so that we can force the IDC_PRINTER_GIF_HEIGHT
	// text field to contain a legal value whenever it loses focus.
	// Question: HOW do you track focus changes? I don't know what message
	// to look for.

	switch (message)
	{
		case WM_INITDIALOG: {
			// Initialize the dialog from the prefs structures
			HWND ctl;
			if (core_settings.matrix_singularmatrix) {
				ctl = GetDlgItem(hDlg, IDC_MATRIX_SINGULARMATRIX);
				SendMessage(ctl, BM_SETCHECK, 1, 0);
			}
			if (core_settings.matrix_outofrange) {
				ctl = GetDlgItem(hDlg, IDC_MATRIX_OUTOFRANGE);
				SendMessage(ctl, BM_SETCHECK, 1, 0);
			}
			if (core_settings.auto_repeat) {
				ctl = GetDlgItem(hDlg, IDC_AUTO_REPEAT);
				SendMessage(ctl, BM_SETCHECK, 1, 0);
			}
			if (state.alwaysOnTop) {
				ctl = GetDlgItem(hDlg, IDC_ALWAYSONTOP);
				SendMessage(ctl, BM_SETCHECK, 1, 0);
			}
			if (state.calculatorKey) {
				ctl = GetDlgItem(hDlg, IDC_CALCULATOR_KEY);
				SendMessage(ctl, BM_SETCHECK, 1, 0);
			}
			if (state.singleInstance) {
				ctl = GetDlgItem(hDlg, IDC_SINGLEINSTANCE);
				SendMessage(ctl, BM_SETCHECK, 1, 0);
			}
			if (state.printerToTxtFile) {
				ctl = GetDlgItem(hDlg, IDC_PRINTER_TXT);
				SendMessage(ctl, BM_SETCHECK, 1, 0);
			}
			SetDlgItemText(hDlg, IDC_PRINTER_TXT_NAME, state.printerTxtFileName);
			if (core_settings.raw_text) {
				ctl = GetDlgItem(hDlg, IDC_RAW_TEXT);
				SendMessage(ctl, BM_SETCHECK, 1, 0);
			}
			if (state.printerToGifFile) {
				ctl = GetDlgItem(hDlg, IDC_PRINTER_GIF);
				SendMessage(ctl, BM_SETCHECK, 1, 0);
			}
			SetDlgItemText(hDlg, IDC_PRINTER_GIF_NAME, state.printerGifFileName);
			SetDlgItemInt(hDlg, IDC_PRINTER_GIF_HEIGHT, state.printerGifMaxLength, TRUE);
			char buf[MAX_PATH];
			get_home_dir(buf, MAX_PATH, FALSE);
			SetDlgItemText(hDlg, IDC_HOMEDIR, buf);
			return TRUE;
		}

		case WM_COMMAND: {
			int cmd = LOWORD(wParam);
			switch (cmd) {
				case IDOK: {
					// Update the prefs structures from the dialog
					HWND ctl = GetDlgItem(hDlg, IDC_MATRIX_SINGULARMATRIX);
					core_settings.matrix_singularmatrix = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
					ctl = GetDlgItem(hDlg, IDC_MATRIX_OUTOFRANGE);
					core_settings.matrix_outofrange = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
					ctl = GetDlgItem(hDlg, IDC_AUTO_REPEAT);
					core_settings.auto_repeat = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
					ctl = GetDlgItem(hDlg, IDC_ALWAYSONTOP);
					BOOL alwaysOnTop = SendMessage(ctl, BM_GETCHECK, 0, 0);
					if (alwaysOnTop != state.alwaysOnTop) {
						state.alwaysOnTop = alwaysOnTop;
						SetWindowPos(hMainWnd, alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
						if (hPrintOutWnd != NULL)
							SetWindowPos(hPrintOutWnd, alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
					}
					ctl = GetDlgItem(hDlg, IDC_CALCULATOR_KEY);
					BOOL prevCalculatorKey = state.calculatorKey;
					state.calculatorKey = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
					if (state.calculatorKey != prevCalculatorKey)
						mapCalculatorKey();
					ctl = GetDlgItem(hDlg, IDC_SINGLEINSTANCE);
					state.singleInstance = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;

					ctl = GetDlgItem(hDlg, IDC_PRINTER_TXT);
					state.printerToTxtFile = SendMessage(ctl, BM_GETCHECK, 0, 0);
					char buf[FILENAMELEN];
					GetDlgItemText(hDlg, IDC_PRINTER_TXT_NAME, buf, FILENAMELEN - 1);
					int len = strlen(buf);
					if (len > 0 && (len < 4 || _stricmp(buf + len - 4, ".txt") != 0))
						strcat(buf, ".txt");
					if (print_txt != NULL && (!state.printerToTxtFile
							|| _stricmp(state.printerTxtFileName, buf) != 0)) {
						fclose(print_txt);
						print_txt = NULL;
					}
					strcpy(state.printerTxtFileName, buf);
					ctl = GetDlgItem(hDlg, IDC_RAW_TEXT);
					core_settings.raw_text = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
					ctl = GetDlgItem(hDlg, IDC_PRINTER_GIF);
					state.printerToGifFile = SendMessage(ctl, BM_GETCHECK, 0, 0);
					BOOL success;
					int maxlen = (int) GetDlgItemInt(hDlg, IDC_PRINTER_GIF_HEIGHT, &success, TRUE);
					state.printerGifMaxLength = !success ? 256 : maxlen < 32 ? 32 : maxlen > 32767 ? 32767 : maxlen;
					GetDlgItemText(hDlg, IDC_PRINTER_GIF_NAME, buf, FILENAMELEN - 1);
					len = strlen(buf);
					if (len > 0 && (len < 4 || _stricmp(buf + len - 4, ".gif") != 0))
						strcat(buf, ".gif");
				    if (print_gif != NULL && (!state.printerToGifFile
							|| _stricmp(state.printerGifFileName, buf) != 0)) {
						shell_finish_gif(gif_seeker, gif_writer);
						fclose(print_gif);
						print_gif = NULL;
					}
					strcpy(state.printerGifFileName, buf);
					char buf2[MAX_PATH];
					GetDlgItemText(hDlg, IDC_HOMEDIR, buf2, MAX_PATH - 1);
					set_home_dir(buf2);
					// fall through
				}
				case IDCANCEL:
					EndDialog(hDlg, LOWORD(wParam));
					return TRUE;
				case IDC_PRINTER_TXT_BROWSE: {
					char buf[FILENAMELEN];
					GetDlgItemText(hDlg, IDC_PRINTER_TXT_NAME, buf, FILENAMELEN - 1);
					if (browse_file(hDlg,
									"Select Text File Name",
									1,
									"Text Files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0\0",
									"txt",
									buf,
									FILENAMELEN))
						SetDlgItemText(hDlg, IDC_PRINTER_TXT_NAME, buf);
					return TRUE;
				}
				case IDC_PRINTER_GIF_BROWSE: {
					char buf[FILENAMELEN];
					GetDlgItemText(hDlg, IDC_PRINTER_GIF_NAME, buf, FILENAMELEN - 1);
					if (browse_file(hDlg,
									"Select GIF File Name",
									1,
									"GIF Files (*.gif)\0*.gif\0All Files (*.*)\0*.*\0\0",
									"gif",
									buf,
									FILENAMELEN))
						SetDlgItemText(hDlg, IDC_PRINTER_GIF_NAME, buf);
					return TRUE;
				}
				case IDC_HOMEDIR_BROWSE: {
					char buf[MAX_PATH];
					GetDlgItemText(hDlg, IDC_HOMEDIR, buf, MAX_PATH - 1);
					config_home_dir(hDlg, buf, MAX_PATH);
					SetDlgItemText(hDlg, IDC_HOMEDIR, buf);
					return TRUE;
				}
			}
			break;
		}
	}
    return FALSE;
}

static int browse_file(HWND owner, char *title, int save, char *filter, char *defExt, char *buf, int buflen) {
	OPENFILENAME ofn;
	ofn.lStructSize = sizeof(OPENFILENAME);
	ofn.hwndOwner = owner;
	ofn.lpstrFilter = filter;
	ofn.lpstrCustomFilter = NULL;
	ofn.lpstrFile = buf;
	ofn.nMaxFile = buflen;
	ofn.lpstrFileTitle = NULL;
	ofn.lpstrInitialDir = NULL;
	ofn.lpstrTitle = title;
	ofn.Flags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT;
	ofn.lpstrDefExt = defExt;
	return save ? GetSaveFileName(&ofn) : GetOpenFileName(&ofn);
}

static void set_home_dir(const char *path) {
	HKEY k1, k2, k3;
	DWORD disp;
	if (RegOpenKeyEx(HKEY_CURRENT_USER, "Software", 0, KEY_QUERY_VALUE, &k1) == ERROR_SUCCESS) {
		if (RegCreateKeyEx(k1, "Thomas Okken Software", 0, "", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &k2, &disp) == ERROR_SUCCESS) {
			if (RegCreateKeyEx(k2, "Free42", 0, "", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &k3, &disp) == ERROR_SUCCESS) {
				RegSetValueEx(k3, "HomeDir", 0, REG_SZ, (const unsigned char *) path, strlen(path) + 1);
				RegCloseKey(k3);
			}
			RegCloseKey(k2);
		}
		RegCloseKey(k1);
	}
}

static void get_home_dir(char *path, int pathlen, BOOL exedir_ok) {
	HKEY k1, k2, k3;
	path[0] = 0;
	BOOL found = FALSE;
	
	if (RegOpenKeyEx(HKEY_CURRENT_USER, "Software", 0, KEY_QUERY_VALUE, &k1) == ERROR_SUCCESS) {
		if (RegOpenKeyEx(k1, "Thomas Okken Software", 0, KEY_QUERY_VALUE, &k2) == ERROR_SUCCESS) {
			if (RegOpenKeyEx(k2, "Free42", 0, KEY_QUERY_VALUE, &k3) == ERROR_SUCCESS) {
				DWORD type, len = pathlen;
				if (RegQueryValueEx(k3, "HomeDir", NULL, &type, (unsigned char *) path, &len) == ERROR_SUCCESS && type == REG_SZ)
					found = TRUE;
				RegCloseKey(k3);
			}
			RegCloseKey(k2);
		}
		RegCloseKey(k1);
	}
	
// Starting with release 1.4, the default Free42 directory is the one
// containing the executable. This means that users upgrading from releases
// 1.1.15 through 1.2.5 will have to manually move their state file etc. from
// <My Documents>\Free42, or manually set the Free42 directory to that location
// in Preferences.
// (Releases 1.1.14 and earlier always used the executable's directory, so we'll
// find it just fine; releases 1.2.6 and later use the Registry to keep track of
// the Free42 directory location, so we'll find it just fine, too.)
// It's an inconvenience, especially since there are probably still mirrors out there
// that carry versions in that range, but for new users, this new default is much
// more intuitive. Plus, it's what most users will want, anyway.
//
//	if (found) {
		if (path[0] == 0 && exedir_ok) {
			GetModuleFileName(0, path, pathlen - 1);
			char *lastbackslash = strrchr(path, '\\');
			if (lastbackslash != 0)
				*lastbackslash = 0;
			else
				strcpy(path, "C:\\Free42");
		}
		return;
//	}

#if 0
	// HomeDir not set: use "My Documents"\Free42 for backward compatibility
	// with releases 1.1.15 and later. Releases 1.1.14 and earlier used different
	// names for the state and print-out files, and stored them in the executable's
	// directory. Users upgrading from such old releases have to manually rename
	// these files.
	LPITEMIDLIST idlist;
	char buf[MAX_PATH];
	if (SHGetSpecialFolderLocation(NULL, CSIDL_PERSONAL, &idlist) == NOERROR) {
		if (!SHGetPathFromIDList(idlist, buf))
			strcpy(buf, "C:");
		strncat(buf, "\\Free42", MAX_PATH - 1);
		buf[MAX_PATH - 1] = 0;
		LPMALLOC imalloc;
		if (SHGetMalloc(&imalloc) == NOERROR)
			imalloc->Free(idlist);
	} else
		strcpy(buf, "C:\\Free42");
	set_home_dir(buf);
	strncpy(path, buf, pathlen - 1);
	path[pathlen - 1] = 0;
#endif
}

static void config_home_dir(HWND owner, char *buf, int bufsize) {
	BROWSEINFO binfo;
	char msg[MAX_PATH + 100];
	sprintf(msg, "Select Free42 directory\n(currently %s)", *buf == 0 ? "unset" : buf);
	binfo.hwndOwner = owner;
	binfo.pidlRoot = NULL;
	binfo.pszDisplayName = buf;
	binfo.lpszTitle = msg;
	binfo.ulFlags = BIF_RETURNONLYFSDIRS;
	binfo.lpfn = NULL;
	binfo.lParam = 0;
	LPITEMIDLIST idlist = SHBrowseForFolder(&binfo);
	if (idlist != NULL) {
		char buf2[MAX_PATH];
		if (SHGetPathFromIDList(idlist, buf2)) {
			strncpy(buf, buf2, bufsize - 1);
			buf[bufsize - 1] = 0;
		}
		LPMALLOC imalloc;
		if (SHGetMalloc(&imalloc) == NOERROR)
			imalloc->Free(idlist);
	}
}

static void mapCalculatorKey() {
	char path[MAX_PATH];
	if (state.calculatorKey) {
		// Get current executable's path
		GetModuleFileName(0, path, MAX_PATH - 1);
	} else {
		// Windows default
		strcpy(path, "calc.exe");
	}
	HKEY k1, k2, k3, k4, k5, k6, k7;
	DWORD disp;
	if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE", 0, KEY_QUERY_VALUE, &k1) == ERROR_SUCCESS) {
		if (RegCreateKeyEx(k1, "Microsoft", 0, "", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &k2, &disp) == ERROR_SUCCESS) {
			if (RegCreateKeyEx(k2, "Windows", 0, "", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &k3, &disp) == ERROR_SUCCESS) {
				if (RegCreateKeyEx(k3, "CurrentVersion", 0, "", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &k4, &disp) == ERROR_SUCCESS) {
					if (RegCreateKeyEx(k4, "Explorer", 0, "", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &k5, &disp) == ERROR_SUCCESS) {
						if (RegCreateKeyEx(k5, "AppKey", 0, "", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &k6, &disp) == ERROR_SUCCESS) {
							if (RegCreateKeyEx(k6, "18", 0, "", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &k7, &disp) == ERROR_SUCCESS) {
								RegSetValueEx(k7, "ShellExecute", 0, REG_SZ, (const unsigned char *) path, strlen(path) + 1);
								RegCloseKey(k7);
							}
							RegCloseKey(k6);
						}
						RegCloseKey(k5);
					}
					RegCloseKey(k4);
				}
				RegCloseKey(k3);
			}
			RegCloseKey(k2);
		}
		RegCloseKey(k1);
	}
}

static void copy() {
	if (!OpenClipboard(hMainWnd))
		return;
	char buf[100];
	core_copy(buf, 100);
	int len = strlen(buf) + 1;
	HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, len);
	if (h != NULL) {
		void *p = GlobalLock(h);
		memcpy(p, buf, len);
		GlobalUnlock(h);
		EmptyClipboard();
		if (SetClipboardData(CF_TEXT, h) == NULL)
			GlobalFree(h);
	}
	CloseClipboard();
}

static void paste() {
	if (!OpenClipboard(hMainWnd))
		return;
	HANDLE h = GetClipboardData(CF_TEXT);
	if (h != NULL) {
		char *p = (char *) GlobalLock(h);
		if (p != NULL) {
			core_paste(p);
			redisplay();
			GlobalUnlock(h);
		}
	}
	CloseClipboard();
}

static void Quit() {
    FILE *printfile = fopen(printfilename, "wb");
    if (printfile != NULL) {
		int length = printout_bottom - printout_top;
		if (length < 0)
			length += PRINT_LINES;
		int n = fwrite(&length, 1, sizeof(int), printfile);
		if (n != sizeof(int))
			goto failed;
		if (printout_bottom >= printout_top) {
			n = fwrite(printout + PRINT_BYTESPERLINE * printout_top,
						   1, PRINT_BYTESPERLINE * length, printfile);
			if (n != PRINT_BYTESPERLINE * length)
				goto failed;
		} else {
			n = fwrite(printout + PRINT_BYTESPERLINE * printout_top,
						   1, PRINT_SIZE - PRINT_BYTESPERLINE * printout_top,
							printfile);
			if (n != PRINT_SIZE - PRINT_BYTESPERLINE * printout_top)
				goto failed;
			n = fwrite(printout, 1,
						   PRINT_BYTESPERLINE * printout_bottom, printfile);
			if (n != PRINT_BYTESPERLINE * printout_bottom)
				goto failed;
		}

		fclose(printfile);
		goto done;

		failed:
		fclose(printfile);
		remove(printfilename);

		done:
		;
    }

	statefile = fopen(statefilename, "wb");
	if (statefile != NULL) {
		if (!placement_saved) {
			GetWindowPlacement(hMainWnd, &state.mainPlacement);
			state.mainPlacementValid = 1;
			if (state.printOutOpen) {
				GetWindowPlacement(hPrintOutWnd, &state.printOutPlacement);
				state.printOutPlacementValid = 1;
			}
		}
		write_shell_state();
	}
	core_quit();
	if (statefile != NULL)
		fclose(statefile);

    if (print_txt != NULL)
		fclose(print_txt);
    
    if (print_gif != NULL) {
		shell_finish_gif(gif_seeker, gif_writer);
		fclose(print_gif);
    }

    shell_spool_exit();
}

static VOID CALLBACK repeater(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime) {
	KillTimer(NULL, timer);
	int repeat = core_repeat();
	if (repeat != 0)
		timer = SetTimer(NULL, 0, repeat == 1 ? 200 : 100, repeater);
	else
		timer = SetTimer(NULL, 0, 250, timeout1);
}

static VOID CALLBACK timeout1(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime) {
	KillTimer(NULL, timer);
	if (ckey != 0) {
		core_keytimeout1();
		timer = SetTimer(NULL, 0, 1750, timeout2);
	} else
		timer = 0;
}

static VOID CALLBACK timeout2(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime) {
	KillTimer(NULL, timer);
	if (ckey != 0)
		core_keytimeout2();
	timer = 0;
}

static VOID CALLBACK timeout3(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime) {
	KillTimer(NULL, timer3);
	bool keep_running = core_timeout3(1);
	timer3 = 0;
	if (keep_running) {
		running = 1;
		// Post dummy message to get the message loop moving again
	    PostMessage(hMainWnd, WM_USER, 0, 0);
	}
}

static VOID CALLBACK battery_checker(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime) {
	shell_low_battery();
}

static void show_printout() {
	if (hPrintOutWnd != NULL) {
		if (IsIconic(hPrintOutWnd))
			OpenIcon(hPrintOutWnd);
		BringWindowToTop(hPrintOutWnd);
		return;
	}

	RECT r;
	SetRect(&r, 0, 0, 286, 600);
	printOutHeight = r.bottom - r.top;
	AdjustWindowRect(&r, WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX
							/*|WS_MAXIMIZEBOX*/|WS_SIZEBOX|WS_OVERLAPPED, 0);

	// TODO: It would be nice if zooming the print-out window would stretch
	// it to the full height of the screen, while leaving its width set to
	// 'printOutWidth'. To do this, I think I'll have to provide my own
	// implementation of the maximize function (maximize box & the maximize
	// item in the window menu), but I don't know how to hook that in. It
	// must be possible, because Snood does it, but I have no idea how.
	// Anyway, since "standard" maximizing (full screen) is not appropriate for
	// the printout window, I just disable the maximize box for now.

	int sbwidth = GetSystemMetrics(SM_CXVSCROLL);

	printOutWidth = r.right - r.left + sbwidth;
	hPrintOutWnd = CreateWindow(szPrintOutWindowClass, szPrintOutTitle,
							WS_CAPTION|WS_SYSMENU|WS_VSCROLL|WS_MINIMIZEBOX
								/*|WS_MAXIMIZEBOX*/|WS_SIZEBOX|WS_OVERLAPPED,
							CW_USEDEFAULT, 0, printOutWidth, printOutHeight,
							NULL, NULL, hInst, NULL);

	if (state.printOutPlacementValid) {
		// Fix the width, in case the saved settings are not appropriate
		// for the current desktop properties (the decor dimensions may
		// be different).
		RECT *r2 = &state.printOutPlacement.rcNormalPosition;
		r2->right = r2->left + printOutWidth;
		SetWindowPlacement(hPrintOutWnd, &state.printOutPlacement);
	}
	if (state.alwaysOnTop)
		SetWindowPos(hPrintOutWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);

	printout_length_changed();

	printout_scroll_to_bottom(0);

	ShowWindow(hPrintOutWnd, SW_SHOW);
	UpdateWindow(hPrintOutWnd);
	state.printOutOpen = 1;
}

static void export_program() {
	if (!DialogBox(hInst, (LPCTSTR)IDD_SELECTPROGRAM, hMainWnd, (DLGPROC)ExportProgram))
		return;
	/* The sel_prog_count global now has the number of selected items;
	 * sel_prog_list is an array of integers containing the item numbers.
	 */
	if (!browse_file(hMainWnd,
					 "Export Programs",
					 1,
					 "Program Files (*.raw)\0*.raw\0All Files (*.*)\0*.*\0\0",
					 "raw",
					 export_file_name,
					 FILENAMELEN))
		return;

	export_file = fopen(export_file_name, "wb");
	if (export_file == NULL) {
		int err = errno;
		char buf[1000];
		sprintf(buf, "Can't open \"%s\" for output: %s (%d)", export_file_name, strerror(err), err);
		MessageBox(hMainWnd, buf, "Message", MB_ICONWARNING);
	} else {
		core_export_programs(sel_prog_count, sel_prog_list, NULL);
		if (export_file != NULL) {
			fclose(export_file);
			export_file = NULL;
		}
	}

	free(sel_prog_list);
}

static void import_program() {
	char buf[FILENAMELEN];
	buf[0] = 0;
	if (!browse_file(hMainWnd,
					 "Import Programs",
					 0,
					 "Program Files (*.raw)\0*.raw\0All Files (*.*)\0*.*\0\0",
					 NULL,
					 buf,
					 FILENAMELEN))
		return;

	import_file = fopen(buf, "rb");
	if (import_file == NULL) {
		char buf[1000];
		int err = errno;
		sprintf(buf, "Could not open \"%s\" for reading: %s (%d)", buf, strerror(err), err);
		MessageBox(hMainWnd, buf, "Message", MB_ICONWARNING);
	} else {
		core_import_programs(NULL);
		redisplay();
		if (import_file != NULL) {
			fclose(import_file);
			import_file = NULL;
		}
	}
}

static void clear_printout() {
	printout_top = 0;
	printout_bottom = 0;
	printout_pos = 0;
	printout_length_changed();
	if (hPrintOutWnd != NULL)
		InvalidateRect(hPrintOutWnd, NULL, FALSE);

	if (print_gif != NULL) {
		shell_finish_gif(gif_seeker, gif_writer);
		fclose(print_gif);
		print_gif = NULL;
    }
}

static void repaint_printout(int x, int y, int width, int height, int validate) {
	HDC hdc = GetDC(hPrintOutWnd);
	repaint_printout(hdc, 0, x, y, width, height, validate);
	ReleaseDC(hPrintOutWnd, hdc);
}

static void repaint_printout(HDC hdc, int destpos, int x, int y,
							 int width, int height, int validate) {
	HDC memdc;
	HBITMAP bitmap;
	int xdest, ydest;

	if (destpos) {
		xdest = x;
		ydest = y;
		y = ydest + printout_pos;
	} else {
		xdest = x;
		ydest = y - printout_pos;
	}

	int printout_length = printout_bottom - printout_top;
	if (printout_length < 0)
		printout_length += PRINT_LINES;
	if (y + height > printout_length) {
		RECT r;
		SetRect(&r, xdest, printout_length - printout_pos,
					xdest + width, ydest + height);
		HBRUSH brush = (HBRUSH) GetStockObject(GRAY_BRUSH);
		SelectObject(hdc, brush);
		FillRect(hdc, &r, brush);
		if (y >= printout_length)
			return;
		height = printout_length - y;
	}

	memdc = CreateCompatibleDC(hdc);
	bitmap = CreateBitmap(286, PRINT_LINES, 1, 1, printout);
	SelectObject(memdc, bitmap);

    if (printout_bottom >= printout_top)
		/* The buffer is not wrapped */
		BitBlt(hdc, xdest, ydest, width, height,
				memdc, x, printout_top + y, SRCCOPY);
    else {
		/* The buffer is wrapped */
		if (printout_top + y < PRINT_LINES) {
			if (printout_top + y + height <= PRINT_LINES)
				/* The rectangle is in the lower part of the buffer */
				BitBlt(hdc, xdest, ydest, width, height,
						memdc, x, printout_top + y, SRCCOPY);
			else {
				/* The rectangle spans both parts of the buffer */
				int part1_height = PRINT_LINES - printout_top - y;
				int part2_height = height - part1_height;
				BitBlt(hdc, xdest, ydest, width, part1_height,
						memdc, x, printout_top + y, SRCCOPY);
				BitBlt(hdc, xdest, ydest + part1_height, width, part2_height,
						memdc, x, 0, SRCCOPY);
			}
		} else
			/* The rectangle is in the upper part of the buffer */
			BitBlt(hdc, xdest, ydest, width, height,
					memdc, x, y + printout_top - PRINT_LINES, SRCCOPY);
    }

	DeleteDC(memdc);
	DeleteObject(bitmap);

	if (validate) {
		RECT r;
		SetRect(&r, xdest, ydest, xdest + width, ydest + height);
		ValidateRect(hPrintOutWnd, &r);
	}
}

static void printout_scrolled(int offset) {
	RECT client;
	GetClientRect(hPrintOutWnd, &client);
	ScrollWindowEx(hPrintOutWnd, 0, offset, &client, &client, NULL, NULL, SW_INVALIDATE);
}

static void printout_scroll_to_bottom(int offset) {
	SCROLLINFO si;
	si.cbSize = sizeof(SCROLLINFO);
	si.fMask = SIF_POS | SIF_RANGE | SIF_PAGE;
	GetScrollInfo(hPrintOutWnd, SB_VERT, &si);
	si.fMask = SIF_POS;
	int p = si.nPage;
	int oldpos = si.nPos;
	if (p > 0)
		p--;
	si.nPos = si.nMax - p;
	printout_pos = si.nPos;
	SetScrollInfo(hPrintOutWnd, SB_VERT, &si, TRUE);
	printout_scrolled(oldpos - printout_pos - offset);
}

static void printout_length_changed() {
	SCROLLINFO si;
	si.cbSize = sizeof(SCROLLINFO);
	si.fMask = SIF_POS;
	GetScrollInfo(hPrintOutWnd, SB_VERT, &si);
	
	int printout_length = printout_bottom - printout_top;
	if (printout_length < 0)
		printout_length += PRINT_LINES;

	si.fMask = SIF_ALL | SIF_DISABLENOSCROLL;
	si.nMin = 0;
	si.nMax = printout_length > 0 ? printout_length - 1 : 0;
	si.nPage = printout_length < printOutHeight ? printout_length : printOutHeight;
	
	int p = si.nPage;
	if (p > 0)
		p--;
	int maxpos = si.nMax - p;
	if (si.nPos > maxpos)
		si.nPos = maxpos;

	printout_pos = si.nPos;
	SetScrollInfo(hPrintOutWnd, SB_VERT, &si, TRUE);
}

void shell_blitter(const char *bits, int bytesperline, int x, int y,
				   int width, int height) {
	HDC hdc = GetDC(hMainWnd);
	skin_display_blitter(hdc, bits, bytesperline, x, y, width, height);
	if (skey >= -7 && skey <= -2) {
		HDC memdc = CreateCompatibleDC(hdc);
		skin_repaint_key(hdc, memdc, skey, 1);
		DeleteObject(memdc);
	}
	ReleaseDC(hMainWnd, hdc);
}

void shell_beeper(int frequency, int duration) {
	Beep(frequency, duration);
}

/* shell_annunciators()
 * Callback invoked by the emulator core to change the state of the display
 * annunciators (up/down, shift, print, run, battery, (g)rad).
 * Every parameter can have values 0 (turn off), 1 (turn on), or -1 (leave
 * unchanged).
 * The battery annunciator is missing from the list; this is the only one of
 * the lot that the emulator core does not actually have any control over, and
 * so the shell is expected to handle that one by itself.
 */
void shell_annunciators(int updn, int shf, int prt, int run, int g, int rad) {
	HDC hdc = GetDC(hMainWnd);
	HDC memdc = CreateCompatibleDC(hdc);

    if (updn != -1 && ann_updown != updn) {
		ann_updown = updn;
		skin_repaint_annunciator(hdc, memdc, 1, ann_updown);
    }
    if (shf != -1 && ann_shift != shf) {
		ann_shift = shf;
		skin_repaint_annunciator(hdc, memdc, 2, ann_shift);
    }
    if (prt != -1 && ann_print != prt) {
		ann_print = prt;
		skin_repaint_annunciator(hdc, memdc, 3, ann_print);
    }
    if (run != -1 && ann_run != run) {
		ann_run = run;
		skin_repaint_annunciator(hdc, memdc, 4, ann_run);
    }
    if (g != -1 && ann_g != g) {
		ann_g = g;
		skin_repaint_annunciator(hdc, memdc, 6, ann_g);
    }
    if (rad != -1 && ann_rad != rad) {
		ann_rad = rad;
		skin_repaint_annunciator(hdc, memdc, 7, ann_rad);
    }

	DeleteDC(memdc);
	ReleaseDC(hMainWnd, hdc);
}

int shell_wants_cpu() {
	MSG msg;
	return PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE) != 0;
}

void shell_delay(int duration) {
	Sleep(duration);
}

/* Callback to ask the shell to call core_timeout3() after the given number of
 * milliseconds. If there are keystroke events during that time, the timeout is
 * cancelled. (Pressing 'shift' does not cancel the timeout.)
 * This function supports the delay after SHOW, MEM, and shift-VARMENU.
 */
void shell_request_timeout3(int delay) {
	if (timer3 != 0)
		KillTimer(NULL, timer3);
	timer3 = SetTimer(NULL, 0, delay, timeout3);
}

int4 shell_read_saved_state(void *buf, int4 bufsize) {
    if (statefile == NULL)
        return -1;
    else {
        int4 n = fread(buf, 1, bufsize, statefile);
        if (n != bufsize && ferror(statefile)) {
            fclose(statefile);
            statefile = NULL;
            return -1;
        } else
            return n;
    }
}

bool shell_write_saved_state(const void *buf, int4 nbytes) {
    if (statefile == NULL)
        return false;
    else {
        int4 n = fwrite(buf, 1, nbytes, statefile);
        if (n != nbytes) {
            fclose(statefile);
            remove(statefilename);
            statefile = NULL;
            return false;
        } else
            return true;
    }
}

uint4 shell_get_mem() {
	MEMORYSTATUS memstat;
	GlobalMemoryStatus(&memstat);
	return memstat.dwAvailPhys;
}

int shell_low_battery() {
	SYSTEM_POWER_STATUS powerstat;
	int lowbat;
	if (!GetSystemPowerStatus(&powerstat))
		lowbat = 0; // getting power status failed; assume we're fine
	else
		lowbat = powerstat.ACLineStatus == 0 // offline
				&& (powerstat.BatteryFlag & 6) != 0; // low or critical
	if (ann_battery != lowbat) {
		ann_battery = lowbat;
		HDC hdc = GetDC(hMainWnd);
		HDC memdc = CreateCompatibleDC(hdc);
		skin_repaint_annunciator(hdc, memdc, 5, ann_battery);
		DeleteDC(memdc);
		ReleaseDC(hMainWnd, hdc);
	}
	return lowbat;
}

void shell_powerdown() {
	PostQuitMessage(0);
}

double shell_random_seed() {
	return ((double) rand()) / (RAND_MAX + 1.0);
}

uint4 shell_milliseconds() {
	return GetTickCount();
}

void shell_get_time_date(uint4 *time, uint4 *date, int *weekday) {
	SYSTEMTIME st;
	GetLocalTime(&st);
	if (time != NULL)
		*time = st.wHour * 1000000 + st.wMinute * 10000 + st.wSecond * 100 + st.wMilliseconds / 10;
	if (date != NULL)
		*date = st.wYear * 10000 + st.wMonth * 100 + st.wDay;
	if (weekday != NULL)
		*weekday = st.wDayOfWeek;
}

void shell_print(const char *text, int length,
		 const char *bits, int bytesperline,
		 int x, int y, int width, int height) {
    int xx, yy;
    int oldlength, newlength;

    for (yy = 0; yy < height; yy++) {
		int4 Y = (printout_bottom + 2 * yy) % PRINT_LINES;
		for (xx = 0; xx < 143; xx++) {
			int bit, px, py;
			if (xx < width) {
				char c = bits[(y + yy) * bytesperline + ((x + xx) >> 3)];
				bit = (c & (1 << ((x + xx) & 7))) != 0;
			} else
				bit = 0;
			for (px = xx * 2; px < (xx + 1) * 2; px++)
				for (py = Y; py < Y + 2; py++)
					if (!bit)
						printout[py * PRINT_BYTESPERLINE + (px >> 3)]
							|= 128 >> (px & 7);
					else
						printout[py * PRINT_BYTESPERLINE + (px >> 3)]
							&= ~(128 >> (px & 7));
		}
    }

    oldlength = printout_bottom - printout_top;
    if (oldlength < 0)
		oldlength += PRINT_LINES;
    printout_bottom = (printout_bottom + 2 * height) % PRINT_LINES;
    newlength = oldlength + 2 * height;

	if (newlength >= PRINT_LINES) {
		printout_top = (printout_bottom + 2) % PRINT_LINES;
		newlength = PRINT_LINES - 2;
		if (hPrintOutWnd != NULL) {
			if (newlength != oldlength)
				printout_length_changed();
			printout_scroll_to_bottom(2 * height + oldlength - newlength);
			repaint_printout(0, newlength - 2 * height, 286, 2 * height, 1);
		}
	} else {
		if (hPrintOutWnd != NULL) {
			printout_length_changed();
			printout_scroll_to_bottom(0);
			repaint_printout(0, oldlength, 286, 2 * height, 1);
		}
	}

    if (state.printerToTxtFile) {
		int err;
		char buf[1000];

		if (print_txt == NULL) {
			print_txt = fopen(state.printerTxtFileName, "ab");
			if (print_txt == NULL) {
				err = errno;
				state.printerToTxtFile = 0;
				sprintf(buf, "Can't open \"%s\" for output: %s (%d)\nPrinting to TXT file disabled.", state.printerTxtFileName, strerror(err), err);
				MessageBox(hMainWnd, buf, "Message", MB_ICONWARNING);
				goto done_print_txt;
			}
		}

		shell_spool_txt(text, length, txt_writer, txt_newliner);
		done_print_txt:;
    }

    if (state.printerToGifFile) {
		int err;
		char buf[1000];
		
		if (print_gif != NULL
			&& gif_lines + height > state.printerGifMaxLength) {
			shell_finish_gif(gif_seeker, gif_writer);
			fclose(print_gif);
			print_gif = NULL;
		}

		if (print_gif == NULL) {
			while (1) {
				int len, p;
				FILE *testfile;

				gif_seq = (gif_seq + 1) % 10000;

				strcpy(print_gif_name, state.printerGifFileName);
				len = strlen(print_gif_name);

				/* Strip ".gif" extension, if present */
				if (len >= 4 &&
					_stricmp(print_gif_name + len - 4, ".gif") == 0) {
					len -= 4;
					print_gif_name[len] = 0;
				}

				/* Strip ".[0-9]+", if present */
				p = len;
				while (p > 0 && print_gif_name[p] >= '0' && print_gif_name[p] <= '9')
					p--;
				if (p < len && p >= 0 && print_gif_name[p] == '.')
					print_gif_name[p] = 0;

				/* Make sure we have enough space for the ".nnnn.gif" */
				p = 1000 - 10;
				print_gif_name[p] = 0;
				p = strlen(print_gif_name);
				sprintf(print_gif_name + p, ".%04d", gif_seq);
				strcat(print_gif_name, ".gif");
				
				/* I know, I know, the civilized thing to do would be to
				 * use stat(2) to find out if the file exists. Another time.
				 * (TODO)
				 */
				testfile = fopen(print_gif_name, "rb");
				if (testfile != NULL)
					fclose(testfile);
				else
					break;
			}
			print_gif = fopen(print_gif_name, "w+b");
			if (print_gif == NULL) {
				err = errno;
				state.printerToGifFile = 0;
				sprintf(buf, "Can't open \"%s\" for output: %s (%d)\nPrinting to GIF file disabled.", print_gif_name, strerror(err), err);
				MessageBox(hMainWnd, buf, "Message", MB_ICONWARNING);
				goto done_print_gif;
			}
			if (!shell_start_gif(gif_writer, state.printerGifMaxLength)) {
				state.printerToGifFile = 0;
				MessageBox(hMainWnd, "Not enough memory for the GIF encoder.\nPrinting to GIF file disabled.", "Message", MB_ICONWARNING);
				goto done_print_gif;
			}
			gif_lines = 0;
		}

		shell_spool_gif(bits, bytesperline, x, y, width, height, gif_writer);
		gif_lines += height;
		done_print_gif:;
    }
}

int shell_write(const char *buf, int4 buflen) {
    int4 written;
    if (export_file == NULL)
		return 0;
    written = fwrite(buf, 1, buflen, export_file);
    if (written != buflen) {
		char buf[1000];
		fclose(export_file);
		export_file = NULL;
		sprintf(buf, "Writing \"%s\" failed.", export_file_name);
		MessageBox(hMainWnd, buf, "Message", MB_ICONWARNING);
		return 0;
    } else
		return 1;
}

int4 shell_read(char *buf, int4 buflen) {
    int4 nread;
    if (import_file == NULL)
		return -1;
    nread = fread(buf, 1, buflen, import_file);
    if (nread != buflen && ferror(import_file)) {
		fclose(import_file);
		import_file = NULL;
		MessageBox(hMainWnd, "An error occurred; import was terminated prematurely.", "Message", MB_ICONWARNING);
		return -1;
    } else
		return nread;
}

shell_bcd_table_struct *shell_get_bcd_table() {
        return NULL;
}

shell_bcd_table_struct *shell_put_bcd_table(shell_bcd_table_struct* bcdtab,
					    uint4 size) {
        return bcdtab;
}

void shell_release_bcd_table(shell_bcd_table_struct *bcdtab) {
	free(bcdtab);
}

extern long keymap_filesize;
extern char keymap_filedata[];

static void read_key_map(const char *keymapfilename) {
	FILE *keymapfile = fopen(keymapfilename, "r");
	int kmcap = 0;
	char line[1024];
	int lineno = 0;

	if (keymapfile == NULL) {
		/* Try to create default keymap file */
		long n;

		keymapfile = fopen(keymapfilename, "wb");
		if (keymapfile == NULL)
			return;
		n = fwrite(keymap_filedata, 1, keymap_filesize, keymapfile);
		if (n != keymap_filesize) {
			int err = errno;
			fprintf(stderr, "Error writing \"%s\": %s (%d)\n",
							keymapfilename, strerror(err), err);
		}
		fclose(keymapfile);

		keymapfile = fopen(keymapfilename, "r");
		if (keymapfile == NULL)
			return;
	}

	while (fgets(line, 1024, keymapfile) != NULL) {
		lineno++;
		keymap_entry *entry = parse_keymap_entry(line, lineno);
		if (entry != NULL) {
			/* Create new keymap entry */
			if (keymap_length == kmcap) {
				kmcap += 50;
				keymap = (keymap_entry *) realloc(keymap, kmcap * sizeof(keymap_entry));
				// TODO - handle memory allocation failure
			}
			memcpy(keymap + (keymap_length++), entry, sizeof(keymap_entry));
		}
	}

	fclose(keymapfile);
}

static void init_shell_state(int4 version) {
	switch (version) {
		case -1:
			state.extras = 0;
			// fall through
		case 0:
			state.mainPlacement.length = sizeof(WINDOWPLACEMENT);
			state.mainPlacementValid = 0;
			state.printOutPlacement.length = sizeof(WINDOWPLACEMENT);
			state.printOutPlacementValid = 0;
			state.printOutOpen = 0;
			// fall through
		case 1:
			state.printerToTxtFile = 0;
			state.printerToGifFile = 0;
			state.printerTxtFileName[0] = 0;
			state.printerGifFileName[0] = 0;
			// fall through
		case 2:
			state.printerGifMaxLength = 256;
			// fall through
		case 3:
			state.skinName[0] = 0;
			// fall through
		case 4:
			state.alwaysOnTop = FALSE;
			// fall through
		case 5:
			state.singleInstance = TRUE;
			// fall through
		case 6:
			state.calculatorKey = FALSE;
			// fall through
		case 7:
			// current version (SHELL_VERSION = 7),
			// so nothing to do here since everything
			// was initialized from the state file.
			;
	}
}

static int read_shell_state(int4 *ver) {
    int4 magic;
    int4 version;
    int4 state_size;
    int4 state_version;

    if (shell_read_saved_state(&magic, sizeof(int4)) != sizeof(int4))
        return 0;
    if (magic != FREE42_MAGIC)
        return 0;

    if (shell_read_saved_state(&version, sizeof(int4)) != sizeof(int4))
        return 0;
    if (version < 0 || version > FREE42_VERSION)
        /* Unknown state file version */
        return 0;

	if (version > 0) {
		if (shell_read_saved_state(&state_size, sizeof(int4)) != sizeof(int4))
			return 0;
		if (shell_read_saved_state(&state_version, sizeof(int4)) != sizeof(int4))
			return 0;
		if (state_version < 0 || state_version > SHELL_VERSION)
			/* Unknown shell state version */
			return 0;
		if (shell_read_saved_state(&state, state_size) != state_size)
			return 0;
		// Initialize the parts of the shell state
		// that were NOT read from the state file
		init_shell_state(state_version);
	} else
		init_shell_state(-1);

	*ver = version;
    return 1;
}

static int write_shell_state() {
    int4 magic = FREE42_MAGIC;
    int4 version = FREE42_VERSION;
    int4 state_size = sizeof(state_type);
    int4 state_version = SHELL_VERSION;

    if (!shell_write_saved_state(&magic, sizeof(int4)))
        return 0;
    if (!shell_write_saved_state(&version, sizeof(int4)))
        return 0;
    if (!shell_write_saved_state(&state_size, sizeof(int4)))
        return 0;
    if (!shell_write_saved_state(&state_version, sizeof(int4)))
        return 0;
    if (!shell_write_saved_state(&state, sizeof(state_type)))
        return 0;

    return 1;
}

/* Callbacks used by shell_print() and shell_spool_txt() / shell_spool_gif() */

static void txt_writer(const char *text, int length) {
    int n;
    if (print_txt == NULL)
		return;
    n = fwrite(text, 1, length, print_txt);
    if (n != length) {
		char buf[1000];
		state.printerToTxtFile = 0;
		fclose(print_txt);
		print_txt = NULL;
		sprintf(buf, "Error while writing to \"%s\".\nPrinting to TXT file disabled", state.printerTxtFileName);
		MessageBox(hMainWnd, buf, "Message", MB_ICONWARNING);
    }
}

static void txt_newliner() {
    if (print_txt == NULL)
		return;
    fputs("\r\n", print_txt);
    fflush(print_txt);
}

static void gif_seeker(int4 pos) {
    if (print_gif == NULL)
		return;
    if (fseek(print_gif, pos, SEEK_SET) == -1) {
		char buf[1000];
		state.printerToGifFile = 0;
		fclose(print_gif);
		print_gif = NULL;
		sprintf(buf, "Error while seeking \"%s\".\nPrinting to GIF file disabled", print_gif_name);
		MessageBox(hMainWnd, buf, "Message", MB_ICONWARNING);
    }
}

static void gif_writer(const char *text, int length) {
    int n;
    if (print_gif == NULL)
		return;
    n = fwrite(text, 1, length, print_gif);
    if (n != length) {
		char buf[1000];
		state.printerToGifFile = 0;
		fclose(print_gif);
		print_gif = NULL;
		sprintf(buf, "Error while writing to \"%s\".\nPrinting to GIF file disabled", print_gif_name);
		MessageBox(hMainWnd, buf, "Message", MB_ICONWARNING);
    }
}
